Skip to main content

pacenotes_installer\wizard\scene/
installation.rs

1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5/// Scene 6: apply the selection to the game folder from the cache. It has no
6/// Back, Next or Cancel: every phase writes to the game folder and the engine
7/// cannot stop between files, and it presses Next itself when the run ends.
8pub unsafe extern "system" fn installation_page(
9    dialog: HWND,
10    message: u32,
11    wparam: WPARAM,
12    lparam: LPARAM,
13) -> isize {
14    match message {
15        wm::WM_INITDIALOG => {
16            crate::wizard::page::adopt_state(dialog, lparam);
17            super::progress_page::init_progress(dialog);
18            1
19        }
20        crate::worker::WM_WORKER_PROGRESS => {
21            let Some(progress) = crate::worker::take_progress(lparam) else {
22                return 0;
23            };
24            if let Some(state) = crate::wizard::page::state(dialog) {
25                if state.is_current_request(wparam) {
26                    super::progress_page::show_progress(dialog, state, &progress);
27                }
28            }
29            1
30        }
31        crate::worker::WM_WORKER_DONE => {
32            let Some(message) = crate::worker::take(lparam) else {
33                return 0;
34            };
35            if let (Some(state), crate::worker::Message::Installed(outcome)) =
36                (crate::wizard::page::state(dialog), message)
37            {
38                if state.is_current_request(wparam) {
39                    finish(dialog, state, outcome);
40                }
41            }
42            1
43        }
44        wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
45            Some(ctl::PSN_SETACTIVE) => {
46                crate::wizard::page::set_sheet_caption(dialog);
47                crate::wizard::page::set_header(dialog, crate::i18n::IDS_S6_TITLE);
48                crate::wizard::page::set_buttons(dialog, 0);
49                crate::ui::enable(
50                    crate::wizard::page::sheet(dialog),
51                    crate::ui::IDCANCEL,
52                    false,
53                );
54                if let Some(state) = crate::wizard::page::state(dialog) {
55                    super::progress_page::show_log(dialog, state);
56                }
57                start(dialog);
58                crate::wizard::page::reply(dialog, 0)
59            }
60            Some(ctl::PSN_QUERYCANCEL) => super::progress_page::on_cancel(dialog),
61            _ => 0,
62        },
63        _ => 0,
64    }
65}
66
67/// Everything it installs from is already in the cache, fetched by scene 4b or
68/// already there on the other routes.
69unsafe fn start(dialog: HWND) {
70    let Some(state) = crate::wizard::page::state(dialog) else {
71        return;
72    };
73    if state.install_started {
74        return;
75    }
76    state.install_started = true;
77    if state.route == crate::wizard::state::Route::Restore {
78        start_restore(dialog, state);
79        return;
80    }
81
82    for key in state.config.install_keys() {
83        super::progress_page::log_line(dialog, state, &format!("selected: {key}"));
84    }
85    // Asked before anything moves, because the tree ticks rows on the user's behalf.
86    if !confirm_removals(dialog, state) {
87        finish(dialog, state, crate::worker::Outcome::Cancelled);
88        return;
89    }
90
91    crate::ui::set_text(
92        dialog,
93        crate::wizard::control_ids::IDC_S6_PHASE,
94        crate::i18n::IDS_S6_CHECKING,
95    );
96    let request = state.next_request();
97    let job = crate::worker::InstallJob {
98        base_url: state.base_url.clone(),
99        cache_root: state.cache.root().to_path_buf(),
100        game_dir: std::path::PathBuf::from(&state.folder),
101        game: state
102            .game
103            .unwrap_or(pacenotes_installer::gamedir::Game::Rbr),
104        selection: state.config.install_keys(),
105        picks: state.config.picks_by_category(),
106        choice_categories: choice_categories(state),
107        facts: host_facts(state),
108        cancel: std::sync::Arc::clone(&state.cancel),
109    };
110    crate::worker::spawn_reporting(dialog, request, move |reporter| {
111        crate::worker::run_install(job, reporter)
112    });
113}
114
115/// Hands the restore the user approved to the worker, its plan unchanged, since
116/// the preview they read is that value.
117unsafe fn start_restore(dialog: HWND, state: &mut crate::wizard::state::WizardState) {
118    let Some(point) = state.restore_to.clone() else {
119        finish(dialog, state, crate::worker::Outcome::Cancelled);
120        return;
121    };
122    let plan = &state.restore_plan;
123    let lines: Vec<String> = std::iter::once(format!("restoring to {}", point.name))
124        .chain(plan.remove.iter().map(|key| format!("to remove: {key}")))
125        .chain(
126            plan.put_back
127                .iter()
128                .map(|wanted| format!("to put back: {}", wanted.key)),
129        )
130        .collect();
131    for line in &lines {
132        super::progress_page::log_line(dialog, state, line);
133    }
134
135    crate::ui::set_text(
136        dialog,
137        crate::wizard::control_ids::IDC_S6_PHASE,
138        crate::i18n::IDS_RESTORE_RUNNING,
139    );
140    let request = state.next_request();
141    let job = crate::worker::RestoreJob {
142        cache_root: state.cache.root().to_path_buf(),
143        game_dir: std::path::PathBuf::from(&state.folder),
144        game: state
145            .game
146            .unwrap_or(pacenotes_installer::gamedir::Game::Rbr),
147        target: point.snapshot,
148        plan: state.restore_plan.clone(),
149        choice_categories: choice_categories(state),
150        facts: host_facts(state),
151    };
152    crate::worker::spawn_reporting(dialog, request, move |reporter| {
153        crate::worker::run_restore(job, reporter)
154    });
155}
156
157fn choice_categories(state: &crate::wizard::state::WizardState) -> Vec<String> {
158    pacenotes_installer::schema::choice_categories(state.game_id())
159        .iter()
160        .map(|category| category.to_string())
161        .collect()
162}
163
164/// What the engine cannot find out for itself, since it stays testable off Windows.
165unsafe fn host_facts(
166    state: &crate::wizard::state::WizardState,
167) -> pacenotes_installer::engine::HostFacts {
168    pacenotes_installer::engine::HostFacts {
169        free_bytes: free_space(&state.folder),
170        game_running: state
171            .game
172            .is_some_and(|game| crate::game_process::is_running_from(game, &state.folder)),
173    }
174}
175
176/// Names every installed package the selection no longer holds and asks
177/// before removing them; false when the user declines.
178unsafe fn confirm_removals(dialog: HWND, state: &mut crate::wizard::state::WizardState) -> bool {
179    let selected = state.config.install_keys();
180    let going: Vec<String> = pacenotes_installer::ledger::Ledger::open(&state.folder)
181        .packages()
182        .keys()
183        .filter(|key| !selected.contains(key))
184        .cloned()
185        .collect();
186    if going.is_empty() {
187        return true;
188    }
189    let question =
190        crate::i18n::load_string_arg(crate::i18n::IDS_S6_CONFIRM_REMOVE, &going.join("\n"));
191    if crate::ui::confirm(dialog, &question) {
192        return true;
193    }
194    super::progress_page::log_line(dialog, state, "the user declined to remove anything");
195    false
196}
197
198/// `None` when it cannot be measured, which makes the engine skip its space check.
199unsafe fn free_space(folder: &str) -> Option<u64> {
200    let wide = crate::i18n::to_wide(folder);
201    let mut free: u64 = 0;
202    let measured = windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
203        wide.as_ptr(),
204        &mut free,
205        core::ptr::null_mut(),
206        core::ptr::null_mut(),
207    );
208    (measured != 0).then_some(free)
209}
210
211/// Records the outcome, reopens the cache the worker wrote, and moves on to the summary.
212unsafe fn finish(
213    dialog: HWND,
214    state: &mut crate::wizard::state::WizardState,
215    outcome: crate::worker::Outcome,
216) {
217    let line = match &outcome {
218        crate::worker::Outcome::Done { applied } => format!(
219            "finished: {} installed, {} removed",
220            applied.installed.len(),
221            applied.uninstalled.len()
222        ),
223        crate::worker::Outcome::Failed(message) => format!("failed: {message}"),
224        crate::worker::Outcome::Cancelled => "cancelled by the user".to_string(),
225    };
226    super::progress_page::log_line(dialog, state, &line);
227    if let crate::worker::Outcome::Done { applied } = &outcome {
228        if let Some(point) = &applied.restore_point {
229            super::progress_page::log_line(dialog, state, &format!("restore point: {point}"));
230        }
231    }
232    state.outcome = Some(outcome);
233    state.cache = pacenotes_installer::cache::Cache::open(state.cache.root().to_path_buf());
234    super::progress_page::finish_page(dialog, crate::i18n::IDS_S6_FINISHED, None);
235}